MySQL 慢查询排查实战:用慢日志与 EXPLAIN 定位性能瓶颈

一、慢查询是怎么悄悄拖垮系统的

一条看似普通的 SQL,在数据量小的时候几毫秒返回,等到表涨到千万级,可能突然变成十几秒。更麻烦的是,慢查询会占用连接、锁住资源、撑满线程池,最终把整个接口拖垮。很多”偶发性”线上故障,根子都在一条被忽视的慢 SQL 上。

排查慢查询并不需要靠直觉猜,MySQL 自带了慢查询日志和 EXPLAIN 执行计划两套武器。把它们用起来,你就能从”感觉哪里慢”变成”精确知道哪一步慢、为什么慢”。

举个真实场景:订单表早期只有几万行,SELECT * FROM orders WHERE user_id=? ORDER BY created_at DESC LIMIT 20 是毫秒级返回;两年后表涨到 8000 万行,同样的语句要从头扫到尾,接口 P99 从 50ms 飙到 12s,还把数据库连接池打满,连累其他接口一起超时。这类问题如果在设计期就建好索引,本可以避免。

二、开启慢查询日志

慢查询日志默认是关闭的。生产环境建议把阈值设成 1 秒(long_query_time=1),并把未走索引的 SQL 也记下来,方便顺藤摸瓜。

# my.cnf / mysqld.cnf
[mysqld]
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = ON

不想改配置文件、只想临时开一下?用管理员账号直接 SET GLOBAL 即可,重启后失效,适合快速定位:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
-- 重新连接后生效,可用下面语句验证
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';

如果你是用 Docker 跑 MySQL,最直接的方式是通过环境变量或挂载配置文件把上面两项打开:

阈值怎么设没有标准答案:高频核心接口可以收紧到 0.1 秒,后台报表类查询容忍到 2 秒也合理。建议先在测试环境用 1 秒跑一周,再根据业务画像逐步收紧。另外慢日志本身有磁盘开销,生产环境长期开启时记得配合日志轮转,别让慢日志把磁盘悄悄写满。

# docker run 方式
docker run -d --name mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  -v $(pwd)/my.cnf:/etc/mysql/conf.d/my.cnf \
  -p 3306:3306 mysql:8.0

三、汇总 TOP 慢 SQL:别盯着单条看

慢日志是原始流水,肉眼翻很累。用 MySQL 自带的 mysqldumpslow 按出现次数和总耗时排序,快速圈出”惯犯”:

# 按总执行时间排序,取前 10 条
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

# 按出现次数排序
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log

如果装了 Percona Toolkit,pt-query-digest 给出的报告更专业,会列出每条 SQL 的查询次数、平均/最大耗时、扫描行数,是做容量评估的好帮手。

看汇总报告时,重点其实不是那条”单发最慢”的偶发 SQL,而是”高频且中等耗时”的那一类——它们单次不吓人,但架不住调用量大,累积起来才是系统的主要负载来源。如果某条 SQL 的 Lock_time 偏高,则提示存在锁竞争,可能是别的长事务一直在持锁,这时候要顺藤摸瓜去查持锁的会话。

慢日志字段含义
Query_time本次执行耗时(秒)
Lock_time等待锁的时间
Rows_sent返回给客户端的行数
Rows_examined引擎扫描的行数(与 sent 差距越大越可疑)

四、用 EXPLAIN 看懂执行计划

找到可疑 SQL 后,在前面加一个 EXPLAIN 就能看到 MySQL 打算怎么执行它。这是定位慢查询最核心的一步,和我们在 PostgreSQL 慢查询排查 里看的执行计划思路相通,只是字段名字不同。

EXPLAIN SELECT * FROM orders
WHERE user_id = 123 AND status = 'paid'
ORDER BY created_at DESC LIMIT 20;
type 值含义(从左到右越来越快)
ALL全表扫描,最危险,必须优化
index全索引扫描,比全表好但仍重
range索引范围扫描,常见且可接受
ref非唯一索引等值匹配
eq_ref / const唯一索引/主键命中,最理想

想看得更细,用 EXPLAIN FORMAT=JSON 或 FORMAT=TRADITIONAL,能直接看到是否用了索引、估算行数、是否发生排序落盘,是判断”为什么慢”的终极放大镜。

EXPLAIN 输出里还有一列不能忽略的 Extra:出现 Using filesort 或 Using temporary 往往意味着额外排序或建临时表,数据量大时非常吃力;而出现 Using index 则是好消息,表示命中了覆盖索引、连回表都省了。把 type、key、rows 和 Extra 串起来看,基本就能还原一条 SQL 的执行全貌。

五、最常见的三类慢查询根因

实战里,慢查询基本逃不出下面三类,按出现频率从高到低排列。

1. 索引失效:在列上套函数、做隐式类型转换、用 LIKE '%关键词' 左模糊,都会让索引失效退化成全表扫描。比如 WHERE DATE(created_at)='2026-01-01' 就一定走不了索引。

其中隐式类型转换尤其隐蔽:当字段是字符串、查询却传了数字时,MySQL 会默默对列做类型转换,导致索引失效却不会报错,执行计划从 ref 一路退化成 ALL。排查时只要看到 EXPLAIN 的 type 是 ALL、rows 又大得离谱,就要回头检查 WHERE 条件里是否对列做了运算或转换。

2. 深分页:LIMIT 100000, 20 这种写法,MySQL 要先读出 100020 行再丢弃前 10 万,越往后越慢。应改成基于上一页最大 ID 的游标分页。

-- 反例:深分页越来越慢
SELECT * FROM orders ORDER BY id DESC LIMIT 100000, 20;

-- 正解:游标分页,利用索引有序
SELECT * FROM orders WHERE id < 上一页最小ID
ORDER BY id DESC LIMIT 20;

3. 大表 JOIN 无索引:关联字段没有索引时,MySQL 只能做嵌套循环甚至笛卡尔积,复杂度瞬间爆炸。务必给 JOIN 的 on 条件字段建索引。

六、索引优化实战

定位到缺失索引后,补索引是最直接的手段。优先建"覆盖索引"——让查询需要的列都落在索引里,引擎连回表都不用:

-- 针对 user_id + status 的过滤与排序建联合索引
CREATE INDEX idx_orders_user_status
  ON orders (user_id, status, created_at DESC);

-- 覆盖索引示例:所需字段都在索引中
SELECT user_id, status, created_at
FROM orders WHERE user_id = 123 ORDER BY created_at DESC;

注意联合索引遵循"最左前缀"原则:查询条件必须从索引最左列开始连续使用才生效。建之前先用 EXPLAIN 验证,避免白建索引还拖慢写入。

但索引也不是越多越好。每多一个索引,写入时就要多维护一份 B+ 树,INSERT、UPDATE、DELETE 都会变慢,磁盘和内存占用也跟着涨。对写入密集的表,要在"读优化"和"写开销"之间权衡,优先为高频查询路径建索引,低频或一次性分析查询可以放到只读从库上去跑。

七、把慢查询监控落进容器与 CI

排查是救火,监控才是防火。除了在 Docker 部署时打开慢日志,更建议把慢查询检查写进 GitHub Actions 这类 CI 流程:在测试库灌入一定量数据后跑关键 SQL,断言其在阈值内返回,从代码合入阶段就拦住慢查询。

# .github/workflows/db.yml 片段
- name: 慢查询回归
  run: |
    mysql -h 127.0.0.1 -u root -p"$DB_PWD" test \
      -e "SET SESSION long_query_time=0.5; \
          SOURCE tests/critical_queries.sql" \
      > query.out
    ! grep -q "Query_time" query.out

更进一步的做法是把慢查询指标接进监控(如 Prometheus + Grafana),用慢查询数量和 P99 耗时画趋势线。一旦某条 SQL 的耗时曲线异常上扬,能在故障发生前就收到告警,而不是等用户反馈才后知后觉。监控与排查是同一件事的两面:前者防复发,后者治已病。

八、排障清单

  • 先开慢日志:确认 long_query_time 与 log_queries_not_using_indexes 已打开。
  • 汇总 TOP:用 mysqldumpslow / pt-query-digest 圈出高频与高耗时的 SQL。
  • 看 EXPLAIN:重点盯 type=ALL、Rows_examined 远大于 Rows_sent。
  • 查索引失效:函数包裹、隐式转换、左模糊、最左前缀断裂。
  • 改写法或加索引:游标分页替代深分页,补覆盖/联合索引并回 EXPLAIN 验证。
  • 进 CI:把关键查询的耗时断言固化进流水线,防止复发。

慢查询排查本质上是一套"开日志 → 汇总 → 看计划 → 找根因 → 优化 → 固化"的闭环。熟练之后,你甚至能在用户还没投诉之前,就从慢日志里把隐患挖出来。它和数据库选型、表结构设计一样,是后端工程师绕不开的基本功。

上一篇 Jenkins 实战:搭建 CI/CD 自动化流水线
下一篇 Embeddings 向量嵌入实战:从原理到语义检索落地
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]